Skip to content

Conversation

@nishika26
Copy link
Contributor

@nishika26 nishika26 commented Jan 29, 2026

This is how the UI looks like right now, please take a pull from this branch to try it out, while the process already starts in the backend -
image

Summary

  • New Features

    • Introduced Knowledge Base page with collection management UI, enabling users to create, view, and delete collections.
    • Added job status tracking functionality for background operations.
    • Added Knowledge Base menu item to navigation sidebar.
  • Improvements

    • Enhanced date formatting with improved error handling and consistent "DD Mon YYYY, HH:MM" display format.

Summary by CodeRabbit

  • New Features

    • Knowledge Base section: create, view, manage document collections with document selection and preview.
    • Background job tracking and polling for collection creation/deletion status.
    • New API proxy endpoints for collection and job operations (authenticated).
  • Improvements

    • Unified, more robust date formatting across the app.
  • UI

    • Added Knowledge Base entry to the sidebar.

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a client Knowledge Base UI, multiple Next.js API proxy routes for collections and jobs that forward X-API-KEY to a backend, centralizes and hardens date formatting, and adds a Knowledge Base menu item to the sidebar.

Changes

Cohort / File(s) Summary
Collections API (list/create)
app/api/collections/route.ts
Added GET proxy for listing collections and POST to create collections; validate X-API-KEY, forward requests to backendUrl/api/v1/collections/, handle empty responses, and propagate backend status and payload.
Collection item API (get/delete)
app/api/collections/[collection_id]/route.ts
Added GET (includes ?include_docs=true) and DELETE; validate X-API-KEY, forward to backend, normalize empty response bodies, and return backend status and payload.
Job status API
app/api/collections/jobs/[job_id]/route.ts
Added GET proxy for job status; forwards X-API-KEY to backendUrl/api/v1/collections/jobs/{job_id}, returns backend payload and status, and returns structured 500 on errors.
Knowledge Base page (client)
app/knowledge-base/page.tsx
New client-side KnowledgeBasePage: two-panel UI for collection CRUD, document selection, optimistic updates, job polling/reconciliation, local caching/enrichment, modals, document preview, and API-key persistence.
Date utils & consumers
app/components/utils.ts, app/document/page.tsx
Made formatDate(dateString?: string) optional and robust (returns 'N/A' or original on parse failure), formats to "day month year, HH:MM"; app/document/page.tsx now imports and uses this utility and removed local implementations.
Sidebar navigation
app/components/Sidebar.tsx
Added a "Knowledge Base" submenu item under Capabilities routing to /knowledge-base.

Sequence Diagram

sequenceDiagram
    participant Client as Client/Browser
    participant UI as KnowledgeBase UI
    participant APIRoute as Next.js API Route
    participant Backend as Backend Service
    participant Poller as Polling Component

    Client->>UI: submit create collection (includes X-API-KEY)
    UI->>APIRoute: POST /api/collections (forward X-API-KEY & body)
    APIRoute->>Backend: POST /api/v1/collections (forward key & body)
    Backend-->>APIRoute: 200 { job_id }
    APIRoute-->>UI: 200 { job_id } (optimistic entry)

    loop polling
        Poller->>APIRoute: GET /api/collections/jobs/{job_id} (X-API-KEY)
        APIRoute->>Backend: GET /api/v1/collections/jobs/{job_id}
        Backend-->>APIRoute: job status
        APIRoute-->>Poller: job status
        Poller->>UI: update collection status / reconcile optimistic entry
    end

    Client->>UI: view collection details
    UI->>APIRoute: GET /api/collections/{collection_id} (X-API-KEY)
    APIRoute->>Backend: GET /api/v1/collections/{collection_id}?include_docs=true
    Backend-->>APIRoute: collection + docs
    APIRoute-->>UI: collection payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • Prajna1999
  • vprashrex

Poem

🐰 I hopped with keys tucked close and bright,
I polled the jobs from dawn to night,
I stitched the dates and menus new,
Optimistic hops made collections true,
hop hop — knowledge blooms in light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Knowledge base: Creation and deletion UI' directly and clearly describes the main changes in the changeset: a new Knowledge Base page with UI for creating and deleting collections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/knowledge_base

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nishika26 nishika26 changed the base branch from main to feature/doc_upload January 30, 2026 05:26
@nishika26 nishika26 changed the base branch from feature/doc_upload to main January 31, 2026 07:45
@nishika26 nishika26 marked this pull request as ready for review February 2, 2026 05:13
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@app/components/utils.ts`:
- Around line 10-21: The formatDate function currently assumes new
Date(dateString) is valid; update formatDate to guard against invalid dates by
checking Number.isNaN(date.getTime()) (or isNaN(date.getTime())) right after
creating const date = new Date(dateString) and before applying the IST offset
and formatting; if the check fails return 'N/A' (or the current fallback) so you
never format an Invalid Date and produce NaN values. Ensure you keep the
existing IST conversion and formatting logic in formatDate unchanged except for
this validity guard.

In `@app/knowledge-base/page.tsx`:
- Around line 170-188: The fetchCollections function assumes the API returns an
object with result.data, so when /api/collections returns a raw array the UI
shows zero items; change the parsing to accept both shapes by treating the
response as const collections = result.data || result || [] before
enriching—then continue to call enrichCollectionWithCache on that array and pass
the enrichedCollections to setCollections.

@nishika26 nishika26 self-assigned this Feb 2, 2026
@nishika26 nishika26 added the enhancement New feature or request label Feb 2, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@app/knowledge-base/page.tsx`:
- Around line 598-609: The onChange handler for the batch size input can set
batchSize to NaN because parseInt(e.target.value) may return NaN; update the
handler used with setBatchSize so you first coerce/validate the parsed value
(e.g., const n = parseInt(e.target.value); const valid = Number.isNaN(n) ? 1 :
n) and then call setBatchSize(Math.max(1, valid)) — modify the inline onChange
arrow function that currently uses setBatchSize(Math.max(1,
parseInt(e.target.value) || 1)) to ensure NaN is handled explicitly before
Math.max.
- Around line 270-275: In handleConfirmCreate add an explicit null guard for
apiKey before using apiKey.key: check if apiKey is null (e.g., if (!apiKey) {
handle the error/return early }) and only call fetch('/api/collections', ...)
when apiKey is non-null; update any UI state (close modal or set an error) in
the early-return branch so TypeScript no longer complains about accessing
apiKey.key and future regressions are prevented.
🧹 Nitpick comments (4)
app/knowledge-base/page.tsx (4)

9-17: Duplicate Document interface definition.

This interface is identical to the one in app/document/page.tsx. Consider importing it from there or extracting to a shared types file to avoid drift and reduce maintenance overhead.

-export interface Document {
-  id: string;
-  fname: string;
-  object_store_url: string;
-  signed_url?: string;
-  file_size?: number;
-  inserted_at?: string;
-  updated_at?: string;
-}
+import { Document } from '@/app/document/page';

79-141: Sequential API calls in enrichCollectionWithCache may cause slow loading.

This function is called via Promise.all on line 185-186, but internally it performs sequential API calls within loops (lines 99, 120). For users with many collections, this creates a waterfall of network requests during enrichment.

Consider batching job status requests or fetching all job statuses in a single call if the backend supports it, then matching locally.


367-372: Missing dependencies in useEffect may cause stale closures.

The effect depends on apiKey but calls fetchCollections and fetchDocuments which are recreated on each render. Either add them to the dependency array with useCallback, or inline the fetch logic.

+  const fetchCollections = useCallback(async () => {
+    // ... existing implementation
+  }, [apiKey]);
+
+  const fetchDocuments = useCallback(async () => {
+    // ... existing implementation  
+  }, [apiKey]);
+
   useEffect(() => {
     if (apiKey) {
       fetchCollections();
       fetchDocuments();
     }
-  }, [apiKey]);
+  }, [apiKey, fetchCollections, fetchDocuments]);

Alternatively, suppress the lint warning if the current behavior is intentional, but document why.


707-711: Use theme color instead of hardcoded hex value.

The delete button uses hardcoded #ef4444 while the project has colors.status.error (#dc2626) defined in the theme. Consider using the theme color for consistency.

                 style={{
                   backgroundColor: 'transparent',
-                  color: '#ef4444',
-                  border: '1px solid `#ef4444`',
+                  color: colors.status.error,
+                  border: `1px solid ${colors.status.error}`,
                 }}

@nishika26 nishika26 changed the title Feature/knowledge base Knowledge base: Creation and deletion UI Feb 4, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@app/knowledge-base/page.tsx`:
- Around line 631-635: The click handler currently always calls
fetchCollectionDetails(collection.id) which triggers failing API calls for
optimistic collections whose ids start with "optimistic-"; update the onClick
logic in the component using setShowCreateForm and setShowDocumentPicker so it
only calls fetchCollectionDetails(collection.id) when collection.id does NOT
startWith "optimistic-"; leave the state updates (setShowCreateForm(false),
setShowDocumentPicker(false)) unchanged and avoid making the network call for
optimistic entries to prevent errors and flicker.
- Around line 667-690: The delete button currently appears for optimistic
(pending) collections and triggers handleDeleteCollection(collection.id) which
calls the DELETE API with an optimistic-* id and fails; update the render logic
to suppress the button for optimistic entries (e.g., when collection.id
startsWith("optimistic-") or collection.pending is true) or change
handleDeleteCollection to detect optimistic ids and remove the collection from
local state without calling the API; locate the button block that references
handleDeleteCollection and collection.id and add the conditional to hide the
button (or route to a local-delete branch) so optimistic collections are not
sent to the DELETE endpoint.
🧹 Nitpick comments (5)
app/knowledge-base/page.tsx (5)

9-17: Consider consolidating the duplicate Document interface.

This Document interface is identical to the one exported from app/document/page.tsx. Duplicating type definitions can lead to drift over time. Consider importing from a shared types file or directly from the document page.

-export interface Document {
-  id: string;
-  fname: string;
-  object_store_url: string;
-  signed_url?: string;
-  file_size?: number;
-  inserted_at?: string;
-  updated_at?: string;
-}
+import { Document } from '../document/page';

Alternatively, create a shared types module (e.g., app/types/index.ts) to house common interfaces.


94-119: Sequential API calls in loop may degrade performance.

When the cache has many entries without collection_id, this loop makes sequential fetchJobStatus calls for each one. With a large cache, this can significantly slow down the initial load.

Consider batching these lookups or limiting the number of iterations to avoid excessive API calls during enrichment.


275-333: Polling iterations may overlap if API calls take longer than 3 seconds.

The setInterval callback is async, but setInterval doesn't wait for the previous iteration to complete. If network requests are slow, multiple polling cycles could run concurrently, causing redundant API calls and potential race conditions when updating state.

Consider adding a guard to skip the iteration if the previous one is still in progress:

Proposed fix
+  const isPollingRef = useRef(false);
+
   pollingRef.current = setInterval(async () => {
+    if (isPollingRef.current) return;
+    isPollingRef.current = true;
+    try {
       const currentApiKey = apiKeyRef.current;
       // ... existing logic ...
+    } finally {
+      isPollingRef.current = false;
+    }
   }, 3000);

496-501: useEffect has missing dependencies that may cause linter warnings.

fetchCollections and fetchDocuments are called inside this effect but are not listed as dependencies. Since these functions are recreated on each render, adding them would cause infinite loops. Consider wrapping them with useCallback to stabilize their references, or add an ESLint disable comment if intentional.

+  // eslint-disable-next-line react-hooks/exhaustive-deps
   useEffect(() => {
     if (apiKey) {
       fetchCollections();
       fetchDocuments();
     }
   }, [apiKey]);

1135-1141: Consider adding sandbox attribute to the iframe for defense-in-depth.

The iframe loads document content from signed URLs. Adding a sandbox attribute restricts the embedded content's capabilities, providing an additional security layer.

   <iframe
     key={previewDoc.id}
     src={previewDoc.signed_url}
     title={previewDoc.fname}
     className="w-full h-full"
-    style={{ border: 'none' }}
+    style={{ border: 'none' }}
+    sandbox="allow-same-origin allow-scripts"
   />

Note: Adjust sandbox permissions based on what the previewed documents require (e.g., allow-scripts only if needed for PDF viewers).

@nishika26 nishika26 linked an issue Feb 6, 2026 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@app/knowledge-base/page.tsx`:
- Around line 286-362: startPolling's interval captures a stale fetchCollections
function; replace the direct call to fetchCollections() with a ref-backed
invocation (e.g., create fetchCollectionsRef.current and assign fetchCollections
to it wherever it's defined) or inline the refresh logic using apiKeyRef so the
interval always uses the latest values. Update references: in startPolling (and
where fetchCollections is defined/assigned) ensure you expose fetchCollections
via fetchCollectionsRef (or move its body into the interval using apiKeyRef) and
call fetchCollectionsRef.current() instead of fetchCollections() so the interval
no longer closes over an outdated function.
- Around line 457-459: The error path in the else branch of the
create-knowledge-base flow calls response.json() unguarded (in
app/knowledge-base/page.tsx) which can throw on non-JSON responses; update that
code where response.json() is called so it safely parses with a fallback (e.g.,
await response.json().catch(() => ({})) or wrap in try/catch) and include the
HTTP status in the alert/log (use response.status and response.statusText) when
constructing the message so you retain HTTP context; look for the exact
unguarded call to response.json() in the create knowledge base handler to apply
this change.
- Line 334: The isComplete calculation is using mismatched case causing every
job to be treated as complete; update the isComplete expression (the line that
defines isComplete using status.toLowerCase()) to check against a lowercase
array of terminal/non-waiting states (e.g.
['pending','processing','in_progress'] or the same set used at the other
location) so that status.toLowerCase() can match correctly and prevent premature
removal from the polling map (refer to the isComplete variable and the status
variable in this file).
🧹 Nitpick comments (3)
app/knowledge-base/page.tsx (3)

9-17: Document interface is duplicated from app/document/page.tsx.

Both files export an identical Document interface. Consider extracting it into a shared types file (e.g., app/types.ts) to avoid drift.


86-148: enrichCollectionWithCache fires N×M network requests on every fetch.

Each collection triggers up to one fetchJobStatus call per uncached job entry in localStorage (lines 100-121), plus another if jobId is found but status is missing (lines 125-139). With many collections and cache entries, this fans out into a large number of sequential-per-collection, parallel-across-collections API calls on every fetchCollections invocation. Consider short-circuiting enrichment for collections that already have a terminal status cached, or batch-fetching job statuses.


984-1022: Modals lack Escape-key handling and focus trapping.

The four modals (confirm create, confirm delete, document picker, document preview) don't listen for the Escape key or trap focus. The UploadDocumentModal in app/document/page.tsx already implements Escape handling — consider applying the same pattern here for consistency and accessibility.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@app/knowledge-base/page.tsx`:
- Around line 86-148: enrichCollectionWithCache currently loops all cached jobs
and calls fetchJobStatus per collection, causing an N×M request burst; instead,
before calling Promise.all over collections, collect the set of uncached job IDs
from the local CACHE_KEY, call fetchJobStatus once per job to build a
jobStatusMap, and then change enrichCollectionWithCache (or its callers) to use
that jobStatusMap for lookups rather than iterating and fetching inside the
function; keep saveCollectionData and fetchJobStatus usages but replace
per-collection cache-iteration with a simple map lookup (or add a new parameter
jobStatusMap to enrichCollectionWithCache) so each job is fetched at most once
per cycle.
- Around line 205-215: The updater passed to setSelectedCollection contains a
side effect (calling fetchCollectionDetails) which must be removed; instead
detect and perform the optimistic->real swap and the fetch outside the state
updater (for example, move the logic into a useEffect that watches
enrichedCollections and selectedCollection), where you find the replacement via
enrichedCollections.find(c => c.job_id === selectedCollection?.job_id), call
setSelectedCollection(replacement) (purely) and then call
fetchCollectionDetails(replacement.id) if a replacement was found; ensure
setSelectedCollection's functional updater no longer calls
fetchCollectionDetails and only returns the new state.
- Around line 986-1027: The delete-confirmation modal rendered when
showConfirmDelete is true needs ARIA roles and keyboard handling: add
role="dialog" and aria-modal="true" to the modal container, set an initial
focusable element (e.g., the Cancel or Delete button) via a ref, implement an
Escape key handler that calls setShowConfirmDelete(false) and clears
setCollectionToDelete(null), and implement a focus-trap that captures
Tab/Shift+Tab to cycle focus among modal focusable elements (use a useEffect in
the component that adds/removes a keydown listener which prevents focus leaving
the modal and restores focus on unmount). Update handlers like
handleConfirmDelete to still close the modal after action; apply the same
pattern later to the document picker and preview modals.
🧹 Nitpick comments (5)
app/knowledge-base/page.tsx (5)

7-7: Importing types from another page component creates tight coupling.

APIKey and STORAGE_KEY are implementation details of the keystore page. If that page is refactored, this import breaks. Consider extracting shared types/constants into a dedicated module (e.g., app/lib/keystore.ts).


541-546: Missing dependencies in effect — fetchCollections and fetchDocuments are not stable.

fetchCollections and fetchDocuments are recreated every render (not wrapped in useCallback), so omitting them from the dependency array silences the exhaustive-deps lint rule but means the effect captures a stale closure on the initial render. This works today only because both functions read apiKey from component scope and the effect re-runs when apiKey changes.

Consider wrapping both in useCallback (with [apiKey] deps) or using refs consistently as you've done for fetchCollectionsRef.


421-421: Hardcoded provider: 'openai'.

This value is not configurable by the user. If additional providers are supported in the future, this will need to change. Consider making it a form field or a configuration constant.


30-1211: Consider splitting this ~1200-line component into smaller pieces.

This single component handles data fetching, polling, caching, optimistic updates, and renders multiple modals + list/detail views. Extracting logical units (e.g., CollectionList, CreateCollectionForm, DocumentPickerModal, custom hooks for polling and collection CRUD) would improve readability and testability.


56-67: localStorage cache is never pruned.

Entries in collection_job_cache are added on create but never removed on delete or after jobs complete. Over time this grows unbounded. Consider cleaning up entries when a collection is deleted or when the job reaches a terminal state.

Comment on lines +986 to +1027
{showConfirmDelete && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div
className="rounded-lg p-6 w-full max-w-md"
style={{ backgroundColor: colors.bg.primary }}
>
<h2 className="text-xl font-semibold mb-4" style={{ color: colors.text.primary }}>
Delete Collection
</h2>
<p className="text-sm mb-6" style={{ color: colors.text.secondary }}>
Are you sure you want to delete this collection? This action cannot be undone.
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setShowConfirmDelete(false);
setCollectionToDelete(null);
}}
className="px-4 py-2 rounded-md text-sm font-medium"
style={{
backgroundColor: 'transparent',
color: colors.text.secondary,
border: `1px solid ${colors.border}`,
}}
>
Cancel
</button>
<button
onClick={handleConfirmDelete}
className="px-4 py-2 rounded-md text-sm font-medium"
style={{
backgroundColor: '#ef4444',
color: '#ffffff',
border: '1px solid #ef4444',
}}
>
Delete
</button>
</div>
</div>
</div>
)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Modals lack keyboard accessibility and ARIA roles.

The confirmation modal (and document picker / preview modals) is missing:

  • role="dialog" and aria-modal="true" attributes
  • Focus trapping (Tab should cycle within the modal)
  • Escape key handler to dismiss

This impacts keyboard-only and screen-reader users.

Minimal improvement for the delete confirmation modal
-        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
+        <div
+          className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
+          role="dialog"
+          aria-modal="true"
+          onKeyDown={(e) => {
+            if (e.key === 'Escape') {
+              setShowConfirmDelete(false);
+              setCollectionToDelete(null);
+            }
+          }}
+        >

Apply similar changes to the document picker and preview modals.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{showConfirmDelete && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div
className="rounded-lg p-6 w-full max-w-md"
style={{ backgroundColor: colors.bg.primary }}
>
<h2 className="text-xl font-semibold mb-4" style={{ color: colors.text.primary }}>
Delete Collection
</h2>
<p className="text-sm mb-6" style={{ color: colors.text.secondary }}>
Are you sure you want to delete this collection? This action cannot be undone.
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setShowConfirmDelete(false);
setCollectionToDelete(null);
}}
className="px-4 py-2 rounded-md text-sm font-medium"
style={{
backgroundColor: 'transparent',
color: colors.text.secondary,
border: `1px solid ${colors.border}`,
}}
>
Cancel
</button>
<button
onClick={handleConfirmDelete}
className="px-4 py-2 rounded-md text-sm font-medium"
style={{
backgroundColor: '#ef4444',
color: '#ffffff',
border: '1px solid #ef4444',
}}
>
Delete
</button>
</div>
</div>
</div>
)}
{showConfirmDelete && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
role="dialog"
aria-modal="true"
onKeyDown={(e) => {
if (e.key === 'Escape') {
setShowConfirmDelete(false);
setCollectionToDelete(null);
}
}}
>
<div
className="rounded-lg p-6 w-full max-w-md"
style={{ backgroundColor: colors.bg.primary }}
>
<h2 className="text-xl font-semibold mb-4" style={{ color: colors.text.primary }}>
Delete Collection
</h2>
<p className="text-sm mb-6" style={{ color: colors.text.secondary }}>
Are you sure you want to delete this collection? This action cannot be undone.
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => {
setShowConfirmDelete(false);
setCollectionToDelete(null);
}}
className="px-4 py-2 rounded-md text-sm font-medium"
style={{
backgroundColor: 'transparent',
color: colors.text.secondary,
border: `1px solid ${colors.border}`,
}}
>
Cancel
</button>
<button
onClick={handleConfirmDelete}
className="px-4 py-2 rounded-md text-sm font-medium"
style={{
backgroundColor: '#ef4444',
color: '#ffffff',
border: '1px solid `#ef4444`',
}}
>
Delete
</button>
</div>
</div>
</div>
)}
🤖 Prompt for AI Agents
In `@app/knowledge-base/page.tsx` around lines 986 - 1027, The delete-confirmation
modal rendered when showConfirmDelete is true needs ARIA roles and keyboard
handling: add role="dialog" and aria-modal="true" to the modal container, set an
initial focusable element (e.g., the Cancel or Delete button) via a ref,
implement an Escape key handler that calls setShowConfirmDelete(false) and
clears setCollectionToDelete(null), and implement a focus-trap that captures
Tab/Shift+Tab to cycle focus among modal focusable elements (use a useEffect in
the component that adds/removes a keydown listener which prevents focus leaving
the modal and restores focus on unmount). Update handlers like
handleConfirmDelete to still close the modal after action; apply the same
pattern later to the document picker and preview modals.

@nishika26
Copy link
Contributor Author

@coderabbitai review please

@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

@nishika26, I'll review the Knowledge Base creation and deletion UI changes for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@app/knowledge-base/page.tsx`:
- Around line 241-256: The updater passed to setSelectedCollection mutates the
outer-scope replacementId via closure which is unsafe in concurrent/StrictMode;
instead compute the replacementId synchronously outside the updater by reading
the current selectedCollection (or a selectedCollectionRef.current) and
searching enrichedCollections (use enrichedCollections.find(c => c.job_id ===
selectedCollection.job_id)), then call setSelectedCollection (or no-op updater)
without side effects, and finally call fetchCollectionDetails(replacementId) if
set; reference setSelectedCollection, replacementId, enrichedCollections,
fetchCollectionDetails and selectedCollectionRef/selectedCollection.
- Around line 576-637: The polling routine pollDeleteStatus currently recurses
forever; modify it to accept and check a retry counter or elapsed startTime
(e.g., maxRetries or maxElapsedMs) and stop polling when the cap is reached
(restore originalCollection and stop further setTimeouts), store the timeout ID
in a ref (e.g., timeoutIdRef) so you can clearTimeout(timeoutIdRef.current), and
ensure the effect that starts pollDeleteStatus returns a cleanup that clears the
timeout and prevents further setState after unmount; reference pollDeleteStatus,
apiKeyRef, jobId, setCollections, setSelectedCollection, and originalCollection
when implementing these changes.
🧹 Nitpick comments (5)
app/knowledge-base/page.tsx (5)

7-7: Consider extracting APIKey and STORAGE_KEY into a shared module.

Importing from another page's module (../keystore/page) creates a tight coupling between pages. If the keystore page is refactored, this import breaks. Move these shared types/constants to a dedicated file (e.g., app/lib/auth.ts).


696-707: fetchCollections / fetchDocuments missing from dependency array; fetchCollections is not memoized.

The effect at line 696 calls fetchCollections() and fetchDocuments() but omits them from the [apiKey] dependency array — this will trigger the react-hooks/exhaustive-deps lint rule. Meanwhile, line 707 lists fetchCollections as a dependency, but since it's not wrapped in useCallback, the ref-sync effect fires on every render.

Consider wrapping fetchCollections and fetchDocuments in useCallback (with [apiKey] as a dependency) so the effects have stable references.


546-668: Extract the repeated restore-on-error logic in handleConfirmDelete.

The pattern of restoring originalCollection into both setCollections and setSelectedCollection is repeated five times in this function. Extract it into a helper to reduce duplication and the risk of inconsistent updates.

Example
+  const restoreCollection = (collectionId: string, original: Collection | undefined) => {
+    if (!original) return;
+    setCollections((prev) =>
+      prev.map((c) => (c.id === collectionId ? original : c))
+    );
+    setSelectedCollection((prev) =>
+      prev?.id === collectionId ? original : prev
+    );
+  };

Then replace each occurrence with restoreCollection(collectionId, originalCollection).


30-50: Consider splitting this ~1400-line component into smaller pieces.

This single component handles data fetching, polling, caching, form state, and renders multiple modals. Extracting custom hooks (e.g., useCollections, useJobPolling) and modal sub-components would improve readability and testability. Not blocking, but worth planning for.


487-492: Hardcoded provider: 'openai'.

The provider is hardcoded. If other providers are supported (or planned), this should be configurable — e.g., via an environment variable or a form field. Fine for an MVP, but worth tracking.

Comment on lines +241 to +256
let replacementId: string | null = null;
setSelectedCollection((prev) => {
if (prev?.id.startsWith('optimistic-') && prev.job_id) {
const replacement = enrichedCollections.find((c: Collection) => c.job_id === prev.job_id);
if (replacement) {
replacementId = replacement.id;
// Don't set the replacement yet - let fetchCollectionDetails do it with full data
}
}
return prev;
});

// Fetch full details (including documents) for the replacement
if (replacementId) {
fetchCollectionDetails(replacementId);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

State updater still has a side effect via closure mutation.

The setSelectedCollection updater at line 242 mutates replacementId (a variable from the outer scope). React may invoke updater functions multiple times in concurrent mode / StrictMode, causing replacementId to be set unpredictably.

Read selectedCollection from a ref or compute the replacement outside the updater entirely:

Proposed fix sketch
-        let replacementId: string | null = null;
-        setSelectedCollection((prev) => {
-          if (prev?.id.startsWith('optimistic-') && prev.job_id) {
-            const replacement = enrichedCollections.find((c: Collection) => c.job_id === prev.job_id);
-            if (replacement) {
-              replacementId = replacement.id;
-            }
-          }
-          return prev;
-        });
-
-        // Fetch full details (including documents) for the replacement
-        if (replacementId) {
-          fetchCollectionDetails(replacementId);
-        }
+        // Compute replacement outside the updater
+        const currentSelected = selectedCollection;
+        if (currentSelected?.id.startsWith('optimistic-') && currentSelected.job_id) {
+          const replacement = enrichedCollections.find((c: Collection) => c.job_id === currentSelected.job_id);
+          if (replacement) {
+            setSelectedCollection(replacement);
+            fetchCollectionDetails(replacement.id);
+          }
+        }
🤖 Prompt for AI Agents
In `@app/knowledge-base/page.tsx` around lines 241 - 256, The updater passed to
setSelectedCollection mutates the outer-scope replacementId via closure which is
unsafe in concurrent/StrictMode; instead compute the replacementId synchronously
outside the updater by reading the current selectedCollection (or a
selectedCollectionRef.current) and searching enrichedCollections (use
enrichedCollections.find(c => c.job_id === selectedCollection.job_id)), then
call setSelectedCollection (or no-op updater) without side effects, and finally
call fetchCollectionDetails(replacementId) if set; reference
setSelectedCollection, replacementId, enrichedCollections,
fetchCollectionDetails and selectedCollectionRef/selectedCollection.

Comment on lines +576 to +637
const pollDeleteStatus = async () => {
const currentApiKey = apiKeyRef.current;
if (!currentApiKey) return;

try {
const jobResponse = await fetch(`/api/collections/jobs/${jobId}`, {
headers: { 'X-API-KEY': currentApiKey.key },
});

if (jobResponse.ok) {
const jobResult = await jobResponse.json();
const jobData = jobResult.data || jobResult;
const status = jobData.status;
const statusLower = status?.toLowerCase();

if (statusLower === 'successful') {
// Job completed successfully - remove from UI
setCollections((prev) => prev.filter((c) => c.id !== collectionId));
setSelectedCollection(null);
} else if (statusLower === 'failed') {
// Job failed - restore original collection
alert('Failed to delete collection');
if (originalCollection) {
setCollections((prev) =>
prev.map((c) => (c.id === collectionId ? originalCollection : c))
);
setSelectedCollection((prev) =>
prev?.id === collectionId ? originalCollection : prev
);
}
} else {
// Still processing - keep status as "deleting" and poll again
setTimeout(pollDeleteStatus, 2000); // Poll every 2 seconds
}
} else {
// Failed to get job status
alert('Failed to check delete status');
if (originalCollection) {
setCollections((prev) =>
prev.map((c) => (c.id === collectionId ? originalCollection : c))
);
setSelectedCollection((prev) =>
prev?.id === collectionId ? originalCollection : prev
);
}
}
} catch (error) {
console.error('Error polling delete status:', error);
alert('Failed to check delete status');
if (originalCollection) {
setCollections((prev) =>
prev.map((c) => (c.id === collectionId ? originalCollection : c))
);
setSelectedCollection((prev) =>
prev?.id === collectionId ? originalCollection : prev
);
}
}
};

// Start polling
pollDeleteStatus();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unbounded delete-status polling with no max retries or unmount cleanup.

pollDeleteStatus recurses via setTimeout indefinitely (line 608) if the job never reaches a terminal state. This leaks timers and can fire setState after unmount.

  1. Add a maximum retry count or total elapsed-time cap.
  2. Track the timeout ID and clear it on unmount.
Sketch
+        let deleteRetries = 0;
+        const MAX_DELETE_RETRIES = 30; // ~60s at 2s intervals
         const pollDeleteStatus = async () => {
+          if (deleteRetries++ >= MAX_DELETE_RETRIES) {
+            alert('Delete operation timed out');
+            if (originalCollection) {
+              setCollections((prev) =>
+                prev.map((c) => (c.id === collectionId ? originalCollection : c))
+              );
+            }
+            return;
+          }
           const currentApiKey = apiKeyRef.current;
           ...
🤖 Prompt for AI Agents
In `@app/knowledge-base/page.tsx` around lines 576 - 637, The polling routine
pollDeleteStatus currently recurses forever; modify it to accept and check a
retry counter or elapsed startTime (e.g., maxRetries or maxElapsedMs) and stop
polling when the cap is reached (restore originalCollection and stop further
setTimeouts), store the timeout ID in a ref (e.g., timeoutIdRef) so you can
clearTimeout(timeoutIdRef.current), and ensure the effect that starts
pollDeleteStatus returns a cleanup that clears the timeout and prevents further
setState after unmount; reference pollDeleteStatus, apiKeyRef, jobId,
setCollections, setSelectedCollection, and originalCollection when implementing
these changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request ready-for-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI: Knowledge base creation

2 participants